from __future__ import absolute_import
import os
import sys
import logging
import time
import tempfile
import base64
from os.path import join, abspath, dirname, exists

import forest.logger
import forest.api
import forest.registry

from forest.registry import get_tools
from forest.registry import get_tool
from forest.api import ConfigurationError
from forest.command import search_commands
from forest.command import Runner
from forest.config import discover_kernel_commandline_opts
from forest.config import Config
from forest.config import DEFAULT_HEADERS
from forest.network import getMacAddress
from forest.network import get_defaultIface
from forest.network import ifaceNameToObjPath
from forest.network import wait_for_network
from forest.tool import search_tools
from forest.uid import getresuid, elevate

logger = logging.getLogger('forest')

INSTALL_SEQUENCE = [
    #'install.RunPreScripts',
    # disk setup
    'install.Partition',
    'install.Raid',
    'install.Format',
    # repo setup
    'pkgr.Prepare',
    'pkgr.Install',
    'pkgr.Run',
    'pkgr.Removes',
    'pkgr.Erase',
    #'yum.Prepare',
    #'yum.Install',
    #'yum.Run',
    #'yum.Strip',
    #'yum.Erase',
    # general purpose setup. post package install
    'setup.PreExecute',
    'setup.Dracut',
    'setup.Fstab',
    'setup.RootPassword',
    'setup.CreateUsers',
    'setup.CreateGroups',
    'setup.KeyboardLayout',
    'setup.Locale',
    'setup.Authconfig',
    'setup.SELinux',
    'setup.TimeZone',
    'setup.Network',
    'setup.SSHKeygen',
    'setup.SSHKeys',
    'setup.Hostname',
    'setup.SysLog',
    'setup.Dbus',
    'files.Append',
    'files.Copy',
    'files.Modify',
    'setup.Systemd',
    'install.RunPostScripts',
    'setup.PostExecute',
    # "output" stages
    #'yum.Mirror',
    #'yum.CreateRepo',
    'install.InstallBootloader',
    'install.RaidPostInstall',
    'install.Cleanup',
]


BUILD_SEQUENCE = [
    'install.RunPreScripts',
    # repo setup
    'pkgr.Prepare',
    'pkgr.Install',
    'pkgr.Run',
    'pkgr.Removes',
    'pkgr.Erase',
    #'yum.Prepare',
    #'yum.Install',
    #'yum.Run',
    #'yum.Strip',
    #'yum.Erase',
    # general purpose setup. post package install
    'setup.Dracut',
    #'setup.Fstab',
    'setup.RootPassword',
    'setup.CreateUsers',
    'setup.CreateGroups',
    'setup.KeyboardLayout',
    'setup.Authconfig',
    'setup.SELinux',
    'setup.TimeZone',
    'setup.Network',
    'setup.SSHKeygen',
    'setup.SSHKeys',
    'setup.Hostname',
    'setup.SysLog',
    'setup.Dbus',
    'files.Append',
    'files.Copy',
    'files.Modify',
    'setup.Systemd',
    'install.RunPostScripts',
    # "output" stages
    #'yum.Mirror',
    #'yum.CreateRepo',
    # forest installer specfic
    'forest.EmbedInstaller',
    'forest.InstallForestService',
    #'forest.EmbedTree',
    'forest.CreateInitrd',
    # end forest installer specfic
    'install.Cleanup',
    'forest.Image',
    # TODO: allow building other types of output (dracut live, etc). Generalize the above into creating initrds.
]

APPLY_SEQUENCE = [
    #'install.RunPreScripts',
    #'pkgr.Prepare',
    #'pkgr.Install',
    #'pkgr.Run',
    #'pkgr.Removes',
    #'pkgr.Erase',
    #'setup.RootPassword',
    #'setup.CreateUsers',
    #'setup.CreateGroups',
    #'setup.KeyboardLayout',
    #'setup.Authconfig',
    #'setup.TimeZone',
    #'setup.Network',
    #'setup.Firewall',
    #'setup.Hostname',
    #'setup.SysLog',
    #'setup.Dbus',
    'setup.PreExecute',
    #'files.Append',
    'files.Copy',
    #'files.Modify',
    #'setup.Systemd',
    'setup.Service',
    'setup.PostExecute',
    #'install.RunPostScripts',
]


def dry_get_tool(toolname):
    def fake_tool(*args, **kwargs):
        print 'DRY RUN', toolname, args, kwargs
    return fake_tool

def replace_get_tool_with_fake():
    forest.api.get_tool = dry_get_tool

class Main(object):
    ''' This class handles everything required to run a initialize a
        configuration object and run a command sequence.

        Arguments:
            args(argparse.Namespace) - The result of ArgumentsParser.parse_args()
    '''
    args = None
    uimodule = None
    root_only = True
    config_obj = None
    sequence = None
    dry_run = False
    config_file = None

    def __init__(self, args):
        self.args = args

        # start the loggers before anything
        setup_loggers(args)

        # start the specified ui
        import forest.ui.cli.handlers as uimodule

        self.uimodule = uimodule
        #import forest.ui.newt.handlers as uimodule
        if hasattr(args, 'debug') and hasattr(args, 'verbose') and (args.debug or args.verbose):
            self.uimodule.DEBUG = True
        self.uimodule.start()

        self._wait_for_network = None
        self._debug = None
        self._logging_level = None
        self.reboot_after = None

    def set_dry_run(self, b):
        '''  Enables/disables dry run mode. In dry runs, no modifications are made.
        '''
        self.dry_run = b

    def get_config(self):
        ''' Returns the Config instance associated with this class.
        '''
        return self.config_obj

    def set_config(self, config):
        ''' Sets this classes Config instance.
        '''
        self.config_obj = config

    def set_config_file(self, filename, prefix = None):
        ''' Loads a tree file from the specified filname and sets this classes Config instance.
        '''
        self.config_file = filename
        print "Loading configuration from file", self.config_file
        c = Config()
        if prefix is not None:
            c.set_prefix(prefix)
        c.load_file(filename)
        
        self.set_config(c)

    def set_sequence(self, s):
        ''' Set the command sequence this class will use.
        '''
        self.sequence = s

    def set_root_only(self, b):
        ''' Enable/disable root only mode. If enabled, only root can run this class.
        '''
        self.root_only = b

    def finish(self):
        ''' Cleanup ourselves after finish. '''
        if self.uimodule:
            self.uimodule.finish()

    def wait_for_network(self):
        ''' Waits for the network to start.  '''
        print "Waiting for network"
        wait_for_network()

    def set_wait_for_network(self, b):
        ''' Enables/disables waiting for the network before running the sequence. '''
        self._wait_for_network = b

    def set_debug(self, b):
        ''' Enables/disables debug mode. '''
        self._debug = b

    def set_logging_level(self, level):
        ''' Sets the global logging level. ? '''
        self._logging_level = level

    def set_reboot_after(self, b):
        ''' Enables/disables rebooting after running the sequence. '''
        self.reboot_after = b

    def run_install(self):
        ''' This method is tailored for tree-install.

            It is meant to run on the target being installed.

            Special kernel command line options (prefixed with tree.) control
            how forest works from the outside.

        '''
        if getattr(self.args, 'wait', False):
            self.wait_for_network()

        #logger.debug(os.environ)
        time.sleep(2)


        print 'RESID', getresuid()
        elevate()
        print 'RESID', getresuid()

        if self.root_only and not os.geteuid()==0:
            sys.exit("\nForest can only be run as root now. Please use sudo or switch to the root user.\n")

        # override the config file if it was specified on the kernel cmd line
        kernel_options = discover_kernel_commandline_opts()

        if kernel_options.has('config') and not self.args.config_file:
            self.set_config_file(kernel_options.get_single('config'))

        # tree.define=yum.architecture=x86_64
        # apply defines from kernel command line to config object
        if kernel_options.has('define'):
            for define in kernel_options.get_multiple('define'):
                key, value = define.split('=', 1)
                self.get_config().set(key, value)

        if kernel_options.has('hostname'):
            self.get_config().set('hostname', kernel_options.get_single('hostname'))

        if kernel_options.has('reboot'):
            self.args.reboot = kernel_options.check_bool_param('reboot')

        if kernel_options.has('wait'):
            self.args.reboot = kernel_options.check_bool_param('wait')

        if kernel_options.has('debug'):
            debug = kernel_options.check_bool_param('debug')
            self.uimodule.DEBUG = debug
            logger.setLevel(logging.DEBUG)
            self.args.debug = True
            self.args.verbose = True

        if kernel_options.has('level'):
            levelname = kernel_options.get_single('level').upper()
            level = getattr(logging, levelname, None)
            if level is not None:
                logger.setLevel(logging.LEVEL)

        self.args.sendmac = kernel_options.check_bool_param('sendmac', False)

        self.args.sendhw = kernel_options.check_bool_param('sendhw', False)

        if kernel_options.has('sshd'):
            # TODO: start the sshd service
            os.system('systemctl start sshd')

        self.args.installer = True
        self.run()

    def run(self):
        ''' Runs the specified sequence.
        '''
        args = self.args
        # TODO: allow for overriding/modifying configuration before it is actually processed

        if hasattr(args, 'define'):
            for define in args.define:
                key, value = define.split('=', 1)
                self.get_config().set(key, value)

        if hasattr(args, 'sendmac') and args.sendmac:
            defaultdev = get_defaultIface()
            ifacename = ifaceNameToObjPath(defaultdev)
            DEFAULT_HEADERS['X-MAC'] = getMacAddress(ifacename)

        # send the output of dmiencode chunked in the headers
        if hasattr(args, 'sendhq') and args.sendhw:
            cmd = 'dmidecode'
            dmilines = '\n'.join(os.popen(cmd).readlines())
            chunks = base64.urlsafe_b64encode(dmilines)
            chunkslen = len(chunks)
            ## 4k per header
            numchunks = int(chunkslen/4000)
            for i in xrange(0, numchunks):
                c = chunks[i*4000:(i*4000)+4000]
                DEFAULT_HEADERS['X-DMI-%d' % ( i)] = c

        config = self.get_config()

        if config is None:
            raise ConfigurationError("No config file or object specified")


        if getattr(args, 'root', False):
            config.set_install_root(args.root)

        if not os.path.lexists( config.get_install_root()):
            os.makedirs(config.get_install_root())

        config.set_build_root(args.build)
        config.set_dry_run(self.dry_run)

        # monkeypatch get_tool **BEFORE** search_commands so that commands will use the proper get_tool
        if self.dry_run:
            replace_get_tool_with_fake()

        if getattr(args, 'installer', False):
            logger.info('is in install mode!')
            logger.info(args)
            config['install_mode'] = True

        config.get_resources().validate()

        # Initialize tools and commands registry
        search_tools()
        tools = get_tools()

        logger.debug('Discovered %s tools: %s', len(tools), sorted(tools.keys()))

        registry = search_commands()

        runner = Runner()
        sequence = self.sequence
        logger.log(5, 'Logger set at level %s', logging.getLogger().getEffectiveLevel())
        if getattr(args, 'jump', None):
            try:
                command_index = sequence.index(args.jump)
            except ValueError:
                raise ConfigurationError("Invalid command name to skip to: %s: %s" % (args.jump, self.sequence))

            # HACK: make the yum object available to scripts if the skip is past yum.Prepare
            # TODO: use requires here
            if command_index > sequence.index('yum.Prepare'):
                runner.run(config, ['yum.Prepare'])

            sequence = sequence[command_index:]

        for skip in getattr(args, 'skip', []):
            skipindex = sequence.index(skip)
            if skipindex == -1:
                raise ConfigurationError("Invalid skip name: %s" % skip)
            del sequence[skipindex]

        try:
            # override the base temp directory for all calls so we can control fs usage
            tempfile.tempdir = join(config.get_build_root())

            if not exists(tempfile.tempdir):
                os.makedirs(tempfile.tempdir)

            tempdir = tempfile.mkdtemp(prefix="forest.")

            # cache the tempdir for commands
            config['tempdir'] = tempdir

            if getattr(args, 'execute', False):

                print registry
                deps = []
                for command in args.execute:
                    com = registry['commands.'+command]
                    print com
                    reqs = getattr(com, 'requires', None)
                    if reqs is None:
                        continue
                    deps += reqs

                runner.run(config, deps)

                sequence = args.execute

            runner.run(config, sequence)
        except Exception:
            tempdir = ''
            raise
        else:
            print "Complete!"
            if getattr(args, 'reboot', False):
                get_tool('system.execute')(['reboot'])
            return 0
            #sys.exit(0)
        finally:
            print "Cleaning up temporary"
            if os.path.exists(tempdir):
                shutil.rmtree(tempdir)

import shutil

def setup_loggers(args):
    logger = logging.getLogger('pkgr')
    logger.setLevel(logging.DEBUG)

    logger = logging.getLogger('forest')
    if not hasattr(args, 'logfile') or not args.logfile:
        return

    mainlogginglevel = logging.DEBUG
    logging.TRACE = 5

    if hasattr(args, "loglevel"):
        valid_log_levels = ['DEBUG', 'INFO', 'WARNING', 'TRACE']
        for mod_level in args.loglevel.split(','):
            split = mod_level.split(':')
            loggername = 'forest'

            if len(split) == 1:
                mainlogginglevel = loggerlevel = split[0]
                continue
            elif len(split) == 2:
                loggername = split[0]
                loggerlevel = split[1]
            else:
                raise Exception("Invalid module:logging-level specification.")

            if loggerlevel not in valid_log_levels:
                raise Exception("Invalid log level %s" % loggerlevel)

            logging.getLogger(loggername).setLevel(getattr(logging, loggerlevel))

    mainlogginglevel = getattr(logging, mainlogginglevel)
    logging.getLogger().setLevel(mainlogginglevel)
    #logging.basicConfig(level=mainlogginglevel,
    #                format='%(asctime)s %(levelname)s %(message)s',
    #                #filename='/tmp/myapp.log',
    #                #filemode='w')
    #                )

    files = args.logfile.split(',')
    for file in files:
        file = file.strip()
        if file == '-':
            forest.logger.init(mainlogginglevel)
        else:

            logfileabs = abspath(args.logfile)
            logfiledir = dirname(logfileabs)
            if not exists(logfiledir):
                os.makedirs(logfiledir)
            print "Logging to", logfileabs
            fileh = logging.FileHandler(logfileabs, mode='a+')
            fileh.setFormatter(logging.Formatter())
            #fileh.setLevel(logging.DEBUG if args.debug else logging.INFO)
            fileh.setLevel(logging.DEBUG)
            rootLogger = logging.getLogger('')
            rootLogger.addHandler(fileh)
            rootLogger = logging.getLogger('tools')
            rootLogger.addHandler(fileh)
    logger.info('Logger set at level %s ', logging.getLogger().getEffectiveLevel())

import argparse

argparser = argparse.ArgumentParser()
argparser.add_argument('config_file', nargs="?", default=None, help="Yaml configuration file.")
argparser.add_argument('-r', '--root', default=False, help="Override the install_root configuration.")
argparser.add_argument('-D', '--define', default=[], action='append', help="Define and override a configuration value. Format key=value. Can be called multiple times")
argparser.add_argument('-l', '--logfile', default=False, help="Output to a logging file.", nargs='?')
argparser.add_argument('-b', '--build', default='cache', help="Build root. Temporary files and outputs are stored here by default.")
argparser.add_argument('-d', '--debug', default=False, action='store_true', help="Debug mode. Outputs heavier logging.")
argparser.add_argument('-L', '--loglevel', default="INFO", help="Logging level or logging level spec.")
argparser.add_argument('--sendmac', default=False, action='store_true', help="Send mac address of active adapter in http headers.")
argparser.add_argument('--sendhw', default=False, action='store_true', help="Send hw informaion in http headers.")
argparser.add_argument('-v', '--verbose', default=False, action='store_true', help="Verbose/debug ui mode. Allow verbose debug message to pass up to the ui.")

